/**
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.smackx.bytestreams.socks5;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;

import org.jivesoftware.smack.AbstractConnectionListener;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.util.SyncPacketSend;
import org.jivesoftware.smackx.ServiceDiscoveryManager;
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
import org.jivesoftware.smackx.bytestreams.BytestreamManager;
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHostUsed;
import org.jivesoftware.smackx.filetransfer.FileTransferManager;
import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.DiscoverItems;
import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
import org.jivesoftware.smackx.packet.DiscoverItems.Item;

/**
 * The Socks5BytestreamManager class handles establishing SOCKS5 Bytestreams as
 * specified in the <a
 * href="http://xmpp.org/extensions/xep-0065.html">XEP-0065</a>.
 * <p>
 * A SOCKS5 Bytestream is negotiated partly over the XMPP XML stream and partly
 * over a separate socket. The actual transfer though takes place over a
 * separately created socket.
 * <p>
 * A SOCKS5 Bytestream generally has three parties, the initiator, the target,
 * and the stream host. The stream host is a specialized SOCKS5 proxy setup on a
 * server, or, the initiator can act as the stream host.
 * <p>
 * To establish a SOCKS5 Bytestream invoke the {@link #establishSession(String)}
 * method. This will negotiate a SOCKS5 Bytestream with the given target JID and
 * return a socket.
 * <p>
 * If a session ID for the SOCKS5 Bytestream was already negotiated (e.g. while
 * negotiating a file transfer) invoke {@link #establishSession(String, String)}.
 * <p>
 * To handle incoming SOCKS5 Bytestream requests add an
 * {@link Socks5BytestreamListener} to the manager. There are two ways to add
 * this listener. If you want to be informed about incoming SOCKS5 Bytestreams
 * from a specific user add the listener by invoking
 * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the
 * listener should respond to all SOCKS5 Bytestream requests invoke
 * {@link #addIncomingBytestreamListener(BytestreamListener)}.
 * <p>
 * Note that the registered {@link Socks5BytestreamListener} will NOT be
 * notified on incoming Socks5 bytestream requests sent in the context of <a
 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer.
 * (See {@link FileTransferManager})
 * <p>
 * If no {@link Socks5BytestreamListener}s are registered, all incoming SOCKS5
 * Bytestream requests will be rejected by returning a &lt;not-acceptable/&gt;
 * error to the initiator.
 * 
 * @author Henning Staib
 */
public final class Socks5BytestreamManager implements BytestreamManager {

	/*
	 * create a new Socks5BytestreamManager and register a shutdown listener on
	 * every established connection
	 */
	static {
		Connection
				.addConnectionCreationListener(new ConnectionCreationListener() {

					public void connectionCreated(final Connection connection) {
						final Socks5BytestreamManager manager;
						manager = Socks5BytestreamManager
								.getBytestreamManager(connection);

						// register shutdown listener
						connection
								.addConnectionListener(new AbstractConnectionListener() {

									public void connectionClosed() {
										manager.disableService();
									}

									public void connectionClosedOnError(
											Exception e) {
										manager.disableService();
									}

									public void reconnectionSuccessful() {
										managers.put(connection, manager);
									}

								});
					}

				});
	}

	/**
	 * The XMPP namespace of the SOCKS5 Bytestream
	 */
	public static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";

	/* prefix used to generate session IDs */
	private static final String SESSION_ID_PREFIX = "js5_";

	/* random generator to create session IDs */
	private final static Random randomGenerator = new Random();

	/* stores one Socks5BytestreamManager for each XMPP connection */
	private final static Map<Connection, Socks5BytestreamManager> managers = new WeakHashMap<Connection, Socks5BytestreamManager>();

	/* XMPP connection */
	private final Connection connection;

	/*
	 * assigns a user to a listener that is informed if a bytestream request for
	 * this user is received
	 */
	private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();

	/*
	 * list of listeners that respond to all bytestream requests if there are
	 * not user specific listeners for that request
	 */
	private final List<BytestreamListener> allRequestListeners = Collections
			.synchronizedList(new LinkedList<BytestreamListener>());

	/* listener that handles all incoming bytestream requests */
	private final InitiationListener initiationListener;

	/*
	 * timeout to wait for the response to the SOCKS5 Bytestream initialization
	 * request
	 */
	private int targetResponseTimeout = 10000;

	/* timeout for connecting to the SOCKS5 proxy selected by the target */
	private int proxyConnectionTimeout = 10000;

	/* blacklist of errornous SOCKS5 proxies */
	private final List<String> proxyBlacklist = Collections
			.synchronizedList(new LinkedList<String>());

	/* remember the last proxy that worked to prioritize it */
	private String lastWorkingProxy = null;

	/* flag to enable/disable prioritization of last working proxy */
	private boolean proxyPrioritizationEnabled = true;

	/*
	 * list containing session IDs of SOCKS5 Bytestream initialization packets
	 * that should be ignored by the InitiationListener
	 */
	private List<String> ignoredBytestreamRequests = Collections
			.synchronizedList(new LinkedList<String>());

	/**
	 * Returns the Socks5BytestreamManager to handle SOCKS5 Bytestreams for a
	 * given {@link Connection}.
	 * <p>
	 * If no manager exists a new is created and initialized.
	 * 
	 * @param connection
	 *            the XMPP connection or <code>null</code> if given connection
	 *            is <code>null</code>
	 * @return the Socks5BytestreamManager for the given XMPP connection
	 */
	public static synchronized Socks5BytestreamManager getBytestreamManager(
			Connection connection) {
		if (connection == null) {
			return null;
		}
		Socks5BytestreamManager manager = managers.get(connection);
		if (manager == null) {
			manager = new Socks5BytestreamManager(connection);
			managers.put(connection, manager);
			manager.activate();
		}
		return manager;
	}

	/**
	 * Private constructor.
	 * 
	 * @param connection
	 *            the XMPP connection
	 */
	private Socks5BytestreamManager(Connection connection) {
		this.connection = connection;
		this.initiationListener = new InitiationListener(this);
	}

	/**
	 * Adds BytestreamListener that is called for every incoming SOCKS5
	 * Bytestream request unless there is a user specific BytestreamListener
	 * registered.
	 * <p>
	 * If no listeners are registered all SOCKS5 Bytestream request are rejected
	 * with a &lt;not-acceptable/&gt; error.
	 * <p>
	 * Note that the registered {@link BytestreamListener} will NOT be notified
	 * on incoming Socks5 bytestream requests sent in the context of <a
	 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file
	 * transfer. (See {@link FileTransferManager})
	 * 
	 * @param listener
	 *            the listener to register
	 */
	public void addIncomingBytestreamListener(BytestreamListener listener) {
		this.allRequestListeners.add(listener);
	}

	/**
	 * Removes the given listener from the list of listeners for all incoming
	 * SOCKS5 Bytestream requests.
	 * 
	 * @param listener
	 *            the listener to remove
	 */
	public void removeIncomingBytestreamListener(BytestreamListener listener) {
		this.allRequestListeners.remove(listener);
	}

	/**
	 * Adds BytestreamListener that is called for every incoming SOCKS5
	 * Bytestream request from the given user.
	 * <p>
	 * Use this method if you are awaiting an incoming SOCKS5 Bytestream request
	 * from a specific user.
	 * <p>
	 * If no listeners are registered all SOCKS5 Bytestream request are rejected
	 * with a &lt;not-acceptable/&gt; error.
	 * <p>
	 * Note that the registered {@link BytestreamListener} will NOT be notified
	 * on incoming Socks5 bytestream requests sent in the context of <a
	 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file
	 * transfer. (See {@link FileTransferManager})
	 * 
	 * @param listener
	 *            the listener to register
	 * @param initiatorJID
	 *            the JID of the user that wants to establish a SOCKS5
	 *            Bytestream
	 */
	public void addIncomingBytestreamListener(BytestreamListener listener,
			String initiatorJID) {
		this.userListeners.put(initiatorJID, listener);
	}

	/**
	 * Removes the listener for the given user.
	 * 
	 * @param initiatorJID
	 *            the JID of the user the listener should be removed
	 */
	public void removeIncomingBytestreamListener(String initiatorJID) {
		this.userListeners.remove(initiatorJID);
	}

	/**
	 * Use this method to ignore the next incoming SOCKS5 Bytestream request
	 * containing the given session ID. No listeners will be notified for this
	 * request and and no error will be returned to the initiator.
	 * <p>
	 * This method should be used if you are awaiting a SOCKS5 Bytestream
	 * request as a reply to another packet (e.g. file transfer).
	 * 
	 * @param sessionID
	 *            to be ignored
	 */
	public void ignoreBytestreamRequestOnce(String sessionID) {
		this.ignoredBytestreamRequests.add(sessionID);
	}

	/**
	 * Disables the SOCKS5 Bytestream manager by removing the SOCKS5 Bytestream
	 * feature from the service discovery, disabling the listener for SOCKS5
	 * Bytestream initiation requests and resetting its internal state.
	 * <p>
	 * To re-enable the SOCKS5 Bytestream feature invoke
	 * {@link #getBytestreamManager(Connection)}. Using the file transfer API
	 * will automatically re-enable the SOCKS5 Bytestream feature.
	 */
	public synchronized void disableService() {

		// remove initiation packet listener
		this.connection.removePacketListener(this.initiationListener);

		// shutdown threads
		this.initiationListener.shutdown();

		// clear listeners
		this.allRequestListeners.clear();
		this.userListeners.clear();

		// reset internal state
		this.lastWorkingProxy = null;
		this.proxyBlacklist.clear();
		this.ignoredBytestreamRequests.clear();

		// remove manager from static managers map
		managers.remove(this.connection);

		// shutdown local SOCKS5 proxy if there are no more managers for other
		// connections
		if (managers.size() == 0) {
			Socks5Proxy.getSocks5Proxy().stop();
		}

		// remove feature from service discovery
		ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager
				.getInstanceFor(this.connection);

		// check if service discovery is not already disposed by connection
		// shutdown
		if (serviceDiscoveryManager != null) {
			serviceDiscoveryManager.removeFeature(NAMESPACE);
		}

	}

	/**
	 * Returns the timeout to wait for the response to the SOCKS5 Bytestream
	 * initialization request. Default is 10000ms.
	 * 
	 * @return the timeout to wait for the response to the SOCKS5 Bytestream
	 *         initialization request
	 */
	public int getTargetResponseTimeout() {
		if (this.targetResponseTimeout <= 0) {
			this.targetResponseTimeout = 10000;
		}
		return targetResponseTimeout;
	}

	/**
	 * Sets the timeout to wait for the response to the SOCKS5 Bytestream
	 * initialization request. Default is 10000ms.
	 * 
	 * @param targetResponseTimeout
	 *            the timeout to set
	 */
	public void setTargetResponseTimeout(int targetResponseTimeout) {
		this.targetResponseTimeout = targetResponseTimeout;
	}

	/**
	 * Returns the timeout for connecting to the SOCKS5 proxy selected by the
	 * target. Default is 10000ms.
	 * 
	 * @return the timeout for connecting to the SOCKS5 proxy selected by the
	 *         target
	 */
	public int getProxyConnectionTimeout() {
		if (this.proxyConnectionTimeout <= 0) {
			this.proxyConnectionTimeout = 10000;
		}
		return proxyConnectionTimeout;
	}

	/**
	 * Sets the timeout for connecting to the SOCKS5 proxy selected by the
	 * target. Default is 10000ms.
	 * 
	 * @param proxyConnectionTimeout
	 *            the timeout to set
	 */
	public void setProxyConnectionTimeout(int proxyConnectionTimeout) {
		this.proxyConnectionTimeout = proxyConnectionTimeout;
	}

	/**
	 * Returns if the prioritization of the last working SOCKS5 proxy on
	 * successive SOCKS5 Bytestream connections is enabled. Default is
	 * <code>true</code>.
	 * 
	 * @return <code>true</code> if prioritization is enabled,
	 *         <code>false</code> otherwise
	 */
	public boolean isProxyPrioritizationEnabled() {
		return proxyPrioritizationEnabled;
	}

	/**
	 * Enable/disable the prioritization of the last working SOCKS5 proxy on
	 * successive SOCKS5 Bytestream connections.
	 * 
	 * @param proxyPrioritizationEnabled
	 *            enable/disable the prioritization of the last working SOCKS5
	 *            proxy
	 */
	public void setProxyPrioritizationEnabled(boolean proxyPrioritizationEnabled) {
		this.proxyPrioritizationEnabled = proxyPrioritizationEnabled;
	}

	/**
	 * Establishes a SOCKS5 Bytestream with the given user and returns the
	 * Socket to send/receive data to/from the user.
	 * <p>
	 * Use this method to establish SOCKS5 Bytestreams to users accepting all
	 * incoming Socks5 bytestream requests since this method doesn't provide a
	 * way to tell the user something about the data to be sent.
	 * <p>
	 * To establish a SOCKS5 Bytestream after negotiation the kind of data to be
	 * sent (e.g. file transfer) use {@link #establishSession(String, String)}.
	 * 
	 * @param targetJID
	 *            the JID of the user a SOCKS5 Bytestream should be established
	 * @return the Socket to send/receive data to/from the user
	 * @throws XMPPException
	 *             if the user doesn't support or accept SOCKS5 Bytestreams, if
	 *             no Socks5 Proxy could be found, if the user couldn't connect
	 *             to any of the SOCKS5 Proxies
	 * @throws IOException
	 *             if the bytestream could not be established
	 * @throws InterruptedException
	 *             if the current thread was interrupted while waiting
	 */
	public Socks5BytestreamSession establishSession(String targetJID)
			throws XMPPException, IOException, InterruptedException {
		String sessionID = getNextSessionID();
		return establishSession(targetJID, sessionID);
	}

	/**
	 * Establishes a SOCKS5 Bytestream with the given user using the given
	 * session ID and returns the Socket to send/receive data to/from the user.
	 * 
	 * @param targetJID
	 *            the JID of the user a SOCKS5 Bytestream should be established
	 * @param sessionID
	 *            the session ID for the SOCKS5 Bytestream request
	 * @return the Socket to send/receive data to/from the user
	 * @throws XMPPException
	 *             if the user doesn't support or accept SOCKS5 Bytestreams, if
	 *             no Socks5 Proxy could be found, if the user couldn't connect
	 *             to any of the SOCKS5 Proxies
	 * @throws IOException
	 *             if the bytestream could not be established
	 * @throws InterruptedException
	 *             if the current thread was interrupted while waiting
	 */
	public Socks5BytestreamSession establishSession(String targetJID,
			String sessionID) throws XMPPException, IOException,
			InterruptedException {

		XMPPException discoveryException = null;
		// check if target supports SOCKS5 Bytestream
		if (!supportsSocks5(targetJID)) {
			throw new XMPPException(targetJID
					+ " doesn't support SOCKS5 Bytestream");
		}

		List<String> proxies = new ArrayList<String>();
		// determine SOCKS5 proxies from XMPP-server
		try {
			proxies.addAll(determineProxies());
		} catch (XMPPException e) {
			// don't abort here, just remember the exception thrown by
			// determineProxies()
			// determineStreamHostInfos() will at least add the local Socks5
			// proxy (if enabled)
			discoveryException = e;
		}

		// determine address and port of each proxy
		List<StreamHost> streamHosts = determineStreamHostInfos(proxies);

		if (streamHosts.isEmpty()) {
			throw discoveryException != null ? discoveryException
					: new XMPPException("no SOCKS5 proxies available");
		}

		// compute digest
		String digest = Socks5Utils.createDigest(sessionID,
				this.connection.getUser(), targetJID);

		// prioritize last working SOCKS5 proxy if exists
		if (this.proxyPrioritizationEnabled && this.lastWorkingProxy != null) {
			StreamHost selectedStreamHost = null;
			for (StreamHost streamHost : streamHosts) {
				if (streamHost.getJID().equals(this.lastWorkingProxy)) {
					selectedStreamHost = streamHost;
					break;
				}
			}
			if (selectedStreamHost != null) {
				streamHosts.remove(selectedStreamHost);
				streamHosts.add(0, selectedStreamHost);
			}

		}

		Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
		try {

			// add transfer digest to local proxy to make transfer valid
			socks5Proxy.addTransfer(digest);

			// create initiation packet
			Bytestream initiation = createBytestreamInitiation(sessionID,
					targetJID, streamHosts);

			// send initiation packet
			Packet response = SyncPacketSend.getReply(this.connection,
					initiation, getTargetResponseTimeout());

			// extract used stream host from response
			StreamHostUsed streamHostUsed = ((Bytestream) response)
					.getUsedHost();
			StreamHost usedStreamHost = initiation.getStreamHost(streamHostUsed
					.getJID());

			if (usedStreamHost == null) {
				throw new XMPPException(
						"Remote user responded with unknown host");
			}

			// build SOCKS5 client
			Socks5Client socks5Client = new Socks5ClientForInitiator(
					usedStreamHost, digest, this.connection, sessionID,
					targetJID);

			// establish connection to proxy
			Socket socket = socks5Client.getSocket(getProxyConnectionTimeout());

			// remember last working SOCKS5 proxy to prioritize it for next
			// request
			this.lastWorkingProxy = usedStreamHost.getJID();

			// negotiation successful, return the output stream
			return new Socks5BytestreamSession(socket, usedStreamHost.getJID()
					.equals(this.connection.getUser()));

		} catch (TimeoutException e) {
			throw new IOException("Timeout while connecting to SOCKS5 proxy");
		} finally {

			// remove transfer digest if output stream is returned or an
			// exception
			// occurred
			socks5Proxy.removeTransfer(digest);

		}
	}

	/**
	 * Returns <code>true</code> if the given target JID supports feature SOCKS5
	 * Bytestream.
	 * 
	 * @param targetJID
	 *            the target JID
	 * @return <code>true</code> if the given target JID supports feature SOCKS5
	 *         Bytestream otherwise <code>false</code>
	 * @throws XMPPException
	 *             if there was an error querying target for supported features
	 */
	private boolean supportsSocks5(String targetJID) throws XMPPException {
		ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager
				.getInstanceFor(this.connection);
		DiscoverInfo discoverInfo = serviceDiscoveryManager
				.discoverInfo(targetJID);
		return discoverInfo.containsFeature(NAMESPACE);
	}

	/**
	 * Returns a list of JIDs of SOCKS5 proxies by querying the XMPP server. The
	 * SOCKS5 proxies are in the same order as returned by the XMPP server.
	 * 
	 * @return list of JIDs of SOCKS5 proxies
	 * @throws XMPPException
	 *             if there was an error querying the XMPP server for SOCKS5
	 *             proxies
	 */
	private List<String> determineProxies() throws XMPPException {
		ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager
				.getInstanceFor(this.connection);

		List<String> proxies = new ArrayList<String>();

		// get all items form XMPP server
		DiscoverItems discoverItems = serviceDiscoveryManager
				.discoverItems(this.connection.getServiceName());
		Iterator<Item> itemIterator = discoverItems.getItems();

		// query all items if they are SOCKS5 proxies
		while (itemIterator.hasNext()) {
			Item item = itemIterator.next();

			// skip blacklisted servers
			if (this.proxyBlacklist.contains(item.getEntityID())) {
				continue;
			}

			try {
				DiscoverInfo proxyInfo;
				proxyInfo = serviceDiscoveryManager.discoverInfo(item
						.getEntityID());
				Iterator<Identity> identities = proxyInfo.getIdentities();

				// item must have category "proxy" and type "bytestream"
				while (identities.hasNext()) {
					Identity identity = identities.next();

					if ("proxy".equalsIgnoreCase(identity.getCategory())
							&& "bytestreams".equalsIgnoreCase(identity
									.getType())) {
						proxies.add(item.getEntityID());
						break;
					}

					/*
					 * server is not a SOCKS5 proxy, blacklist server to skip
					 * next time a Socks5 bytestream should be established
					 */
					this.proxyBlacklist.add(item.getEntityID());

				}
			} catch (XMPPException e) {
				// blacklist errornous server
				this.proxyBlacklist.add(item.getEntityID());
			}
		}

		return proxies;
	}

	/**
	 * Returns a list of stream hosts containing the IP address an the port for
	 * the given list of SOCKS5 proxy JIDs. The order of the returned list is
	 * the same as the given list of JIDs excluding all SOCKS5 proxies who's
	 * network settings could not be determined. If a local SOCKS5 proxy is
	 * running it will be the first item in the list returned.
	 * 
	 * @param proxies
	 *            a list of SOCKS5 proxy JIDs
	 * @return a list of stream hosts containing the IP address an the port
	 */
	private List<StreamHost> determineStreamHostInfos(List<String> proxies) {
		List<StreamHost> streamHosts = new ArrayList<StreamHost>();

		// add local proxy on first position if exists
		List<StreamHost> localProxies = getLocalStreamHost();
		if (localProxies != null) {
			streamHosts.addAll(localProxies);
		}

		// query SOCKS5 proxies for network settings
		for (String proxy : proxies) {
			Bytestream streamHostRequest = createStreamHostRequest(proxy);
			try {
				Bytestream response = (Bytestream) SyncPacketSend.getReply(
						this.connection, streamHostRequest);
				streamHosts.addAll(response.getStreamHosts());
			} catch (XMPPException e) {
				// blacklist errornous proxies
				this.proxyBlacklist.add(proxy);
			}
		}

		return streamHosts;
	}

	/**
	 * Returns a IQ packet to query a SOCKS5 proxy its network settings.
	 * 
	 * @param proxy
	 *            the proxy to query
	 * @return IQ packet to query a SOCKS5 proxy its network settings
	 */
	private Bytestream createStreamHostRequest(String proxy) {
		Bytestream request = new Bytestream();
		request.setType(IQ.Type.GET);
		request.setTo(proxy);
		return request;
	}

	/**
	 * Returns the stream host information of the local SOCKS5 proxy containing
	 * the IP address and the port or null if local SOCKS5 proxy is not running.
	 * 
	 * @return the stream host information of the local SOCKS5 proxy or null if
	 *         local SOCKS5 proxy is not running
	 */
	private List<StreamHost> getLocalStreamHost() {

		// get local proxy singleton
		Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();

		if (socks5Server.isRunning()) {
			List<String> addresses = socks5Server.getLocalAddresses();
			int port = socks5Server.getPort();

			if (addresses.size() >= 1) {
				List<StreamHost> streamHosts = new ArrayList<StreamHost>();
				for (String address : addresses) {
					StreamHost streamHost = new StreamHost(
							this.connection.getUser(), address);
					streamHost.setPort(port);
					streamHosts.add(streamHost);
				}
				return streamHosts;
			}

		}

		// server is not running or local address could not be determined
		return null;
	}

	/**
	 * Returns a SOCKS5 Bytestream initialization request packet with the given
	 * session ID containing the given stream hosts for the given target JID.
	 * 
	 * @param sessionID
	 *            the session ID for the SOCKS5 Bytestream
	 * @param targetJID
	 *            the target JID of SOCKS5 Bytestream request
	 * @param streamHosts
	 *            a list of SOCKS5 proxies the target should connect to
	 * @return a SOCKS5 Bytestream initialization request packet
	 */
	private Bytestream createBytestreamInitiation(String sessionID,
			String targetJID, List<StreamHost> streamHosts) {
		Bytestream initiation = new Bytestream(sessionID);

		// add all stream hosts
		for (StreamHost streamHost : streamHosts) {
			initiation.addStreamHost(streamHost);
		}

		initiation.setType(IQ.Type.SET);
		initiation.setTo(targetJID);

		return initiation;
	}

	/**
	 * Responses to the given packet's sender with a XMPP error that a SOCKS5
	 * Bytestream is not accepted.
	 * 
	 * @param packet
	 *            Packet that should be answered with a not-acceptable error
	 */
	protected void replyRejectPacket(IQ packet) {
		XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
		IQ errorIQ = IQ.createErrorResponse(packet, xmppError);
		this.connection.sendPacket(errorIQ);
	}

	/**
	 * Activates the Socks5BytestreamManager by registering the SOCKS5
	 * Bytestream initialization listener and enabling the SOCKS5 Bytestream
	 * feature.
	 */
	private void activate() {
		// register bytestream initiation packet listener
		this.connection.addPacketListener(this.initiationListener,
				this.initiationListener.getFilter());

		// enable SOCKS5 feature
		enableService();
	}

	/**
	 * Adds the SOCKS5 Bytestream feature to the service discovery.
	 */
	private void enableService() {
		ServiceDiscoveryManager manager = ServiceDiscoveryManager
				.getInstanceFor(this.connection);
		if (!manager.includesFeature(NAMESPACE)) {
			manager.addFeature(NAMESPACE);
		}
	}

	/**
	 * Returns a new unique session ID.
	 * 
	 * @return a new unique session ID
	 */
	private String getNextSessionID() {
		StringBuilder buffer = new StringBuilder();
		buffer.append(SESSION_ID_PREFIX);
		buffer.append(Math.abs(randomGenerator.nextLong()));
		return buffer.toString();
	}

	/**
	 * Returns the XMPP connection.
	 * 
	 * @return the XMPP connection
	 */
	protected Connection getConnection() {
		return this.connection;
	}

	/**
	 * Returns the {@link BytestreamListener} that should be informed if a
	 * SOCKS5 Bytestream request from the given initiator JID is received.
	 * 
	 * @param initiator
	 *            the initiator's JID
	 * @return the listener
	 */
	protected BytestreamListener getUserListener(String initiator) {
		return this.userListeners.get(initiator);
	}

	/**
	 * Returns a list of {@link BytestreamListener} that are informed if there
	 * are no listeners for a specific initiator.
	 * 
	 * @return list of listeners
	 */
	protected List<BytestreamListener> getAllRequestListeners() {
		return this.allRequestListeners;
	}

	/**
	 * Returns the list of session IDs that should be ignored by the
	 * InitialtionListener
	 * 
	 * @return list of session IDs
	 */
	protected List<String> getIgnoredBytestreamRequests() {
		return ignoredBytestreamRequests;
	}

}
